/*
 * Copyright (c) 2006-2011, Petr Panteleyev
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 *    3. The name of the author may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.panteleyev.livejournal;

import com.google.common.collect.ImmutableList;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventObject;
import java.util.concurrent.ExecutionException;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.DefaultCellEditor;
import javax.swing.DropMode;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;

import org.jdesktop.swingx.JXStatusBar;
import org.panteleyev.blogclient.AboutDialog;
import org.panteleyev.blogclient.BloggerFrame;
import org.panteleyev.blogclient.BloggingKit;
import org.panteleyev.blogclient.Icon;
import org.panteleyev.blogclient.MainClass;
import org.panteleyev.blogclient.Options;
import org.panteleyev.livejournal.events.BanListUpdatedEvent;
import org.panteleyev.ljapi.LJAccount;
import org.panteleyev.ljapi.LJException;
import org.panteleyev.ljapi.LJFriend;
import org.panteleyev.ljapi.LJFriendAlphabeticalComparator;
import org.panteleyev.ljapi.LJFriendChange;
import org.panteleyev.ljapi.LJFriendListStatistics;
import org.panteleyev.ljapi.LJGroup;
import org.panteleyev.ljapi.LJJournalType;
import org.panteleyev.swing.BaseTable;
import org.panteleyev.swing.OSUtil;
import org.panteleyev.swing.SimpleListModel;
import org.panteleyev.swing.SwingFactory;
import org.panteleyev.swing.TableUtil;
import org.panteleyev.utilities.Updatable;
import org.panteleyev.utilities.events.AppEventListener;

/**
 *
 * @author Petr Panteleyev
 */
public class FriendsFrame extends BloggerFrame<LJUserProfile> implements Updatable {
    class GroupListCellRenderer extends JLabel implements ListCellRenderer<LJGroup> {
        public GroupListCellRenderer() {
            setOpaque(true);
        }

        @Override
        public Component getListCellRendererComponent(JList list, LJGroup group, int index, boolean isSelected, boolean cellHasFocus) {
            setText(group.getName());
            setBackground(isSelected ? list.getSelectionBackground() : list.getBackground());
            setForeground(isSelected ? list.getSelectionForeground() : list.getForeground());

            ImageIcon icon = (group == null || group.isPublic())? Icon.EMPTY.i() : Icon.LOCK.i();
            setIcon(icon);
            return this;
        }
    }

    class FriendListCellRenderer extends JLabel implements ListCellRenderer<LJFriend> {
        public FriendListCellRenderer() {
            setOpaque(true);
        }

        @Override
        public Component getListCellRendererComponent(JList list, LJFriend value, int index, boolean isSelected, boolean cellHasFocus) {
            setText(value.getUserName());
            setBackground(isSelected ? list.getSelectionBackground() : list.getBackground());
            setForeground(isSelected ? list.getSelectionForeground() : list.getForeground());
            return this;
        }
    }

    private LJFriendListStatistics stat = null;

    private abstract class FriendOperationThread extends SwingWorker<Object,Object> {
        FriendOperationThread() {
            getBusyLabel().setBusy(true);
        }

        @Override
        protected void done() {
            try {
                get();
            } catch (InterruptedException | ExecutionException ex) {
                handleException(ex);
            } finally {
                onFriendListUpdated();
                getProfile().sendApplicationEvent(new FriendListUpdatedEvent(FriendsFrame.this));
                getBusyLabel().setBusy(false);
            }
        }
    }

    private abstract class BanTableThread extends SwingWorker<Object,Object> {
        private ArrayList<String> res = new ArrayList<>();

        protected void reloadBanList() throws Exception {
            m_account.consoleCommand("ban_list", res);

            ArrayList<LJBannedUser> newList = new ArrayList<>();

            for (String r : res) {
                /* look if this one alrady in the list */
                boolean found = false;
                for (LJBannedUser old : getProfile().getBannedUsers()) {
                    if (old.getUserName().equals(r)) {
                        newList.add(old);
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    LJBannedUser newUser = new LJBannedUser(r);
                    newList.add(newUser);
                }
            }

            getProfile().setBannedUsers(newList);
        }
    }

    private class UpdateBanTableThread extends BanTableThread {
        UpdateBanTableThread() {
            getBusyLabel().setBusy(true);
        }

        @Override
        protected Object doInBackground() throws Exception {
            reloadBanList();
            return null;
        }

        @Override
        protected void done() {
            onBannedTableUpdated();
            getBusyLabel().setBusy(false);
        }
    }

    private class BanNonFriendThread extends BanTableThread {
        private String uName;

        BanNonFriendThread(String uName) {
            this.uName = uName;
        }

        @Override
        protected Object doInBackground() throws Exception {
            m_account.ban_set(uName);
            reloadBanList();
            return null;
        }

        @Override
        protected void done() {
            onBannedTableUpdated();
            getBusyLabel().setBusy(false);
        }
    }

    private class BanFriendThread extends BanTableThread {
        private LJFriend friend;

        BanFriendThread(LJFriend friend) {
            this.friend = friend;
        }

        @Override
        protected Object doInBackground() throws Exception {
            m_account.banFriend(friend);
            reloadBanList();
            return null;
        }

        @Override
        protected void done() {
            onBannedTableUpdated();
            onFriendListUpdated();
            getProfile().sendApplicationEvent(new FriendListUpdatedEvent(FriendsFrame.this));
            getBusyLabel().setBusy(false);
        }
    }

    private class UnbanNonFriendThread extends BanTableThread {
        private String uName;

        UnbanNonFriendThread(String uName) {
            this.uName = uName;
        }

        @Override
        protected Object doInBackground() throws Exception {
            m_account.ban_unset(uName);
            reloadBanList();
            return null;
        }

        @Override
        protected void done() {
            onBannedTableUpdated();
            getBusyLabel().setBusy(false);
        }
    }

    private class UpdateFriendsThread extends SwingWorker<Object,Object> {
        private Exception ex;
        ArrayList<LJFriendChange> m_change;

        UpdateFriendsThread() {
            getBusyLabel().setBusy(true);
        }

        @Override
        protected Object doInBackground() throws Exception {
            try {
                m_change = (m_account.getFriendsCount() == 0)? null : new ArrayList<LJFriendChange>();
                m_account.loadFriends(true, m_change, null);
                stat = m_account.getCounters();
            } catch (IOException | LJException e) {
                ex = e;
            }

            return null;
        }

        @Override
        protected void done() {
            if (ex == null) {
                if (m_change != null && !m_change.isEmpty()) {
                    /* Show friend list changes */
                    new FriendListChangeDialog(FriendsFrame.this, m_change).showDialog();
                }
            } else {
                handleException(ex);
            }

            onFriendListUpdated();
            getProfile().sendApplicationEvent(new FriendListUpdatedEvent(FriendsFrame.this));
            getBusyLabel().setBusy(false);
        }
    }

    private final class FriendListModel<T> extends SimpleListModel<T> {
        private ArrayList<T> list;
        FriendListModel(ArrayList<T> list) {
            this.list = list;
        }
        @Override
        public int getSize() {
            return list.size();
        }
        @Override
        public T getElementAt(int index) {
            return list.get(index);
        }
    };

    private AbstractTableModel          friendsListTableModel;
    private AbstractTableModel          bannedTableModel;
    private FriendListModel<LJGroup>    groupListModel;
    private FriendListModel<LJFriend>   friendsNotInListModel;
    private FriendListModel<LJFriend>   friendsInListModel;
    private LJAccount m_account;
    private ArrayList<LJFriend>         _friends = new ArrayList<>();
    private ArrayList<LJFriend>         friendsIn;
    private ArrayList<LJFriend>         friendsNotIn;

    /* Local copy for groups */
    private ArrayList<LJGroup>          groupsCopy;
    private ArrayList<LJGroup>          groupsToDelete;
    private int                         groupBitMap;
    private boolean                     groupsChanged;
    private ArrayList<LJFriend>         changedFriends;

    /* Remember selected tab */
    private int                         selectedTab;

    /* Actions */
    private AbstractAction addFriendAction;
    private AbstractAction removeFriendAction;
    private AbstractAction banFriendAction;
    private AbstractAction friendJournalAction;
    private AbstractAction friendInfoAction;
    private AbstractAction friendCalendarAction;
    private AbstractAction friendFriendsAction;
    private AbstractAction addGroupAction;
    private AbstractAction removeGroupAction;
    private AbstractAction groupUpAction;
    private AbstractAction groupDownAction;
    private AbstractAction groupTogglePublicAction;
    private AbstractAction saveGroupsAction;
    private AbstractAction banAction;
    private AbstractAction unbanAction;

    private JXStatusBar                 statusBar = new JXStatusBar();
    private BaseTable                   banTable;
    private JScrollPane                 banTableScroller;
    private BaseTable                   friendsTable;
    private JScrollPane                 friendListScroller;
    private JLabel                      friendCountEdit = new JLabel();
    private JLabel                      mutualCountEdit = new JLabel();
    private JLabel                      friendofCountEdit = new JLabel();
    private JLabel                      communityCountEdit = new JLabel();
    private JLabel                      synCountEdit = new JLabel();
    private JList<LJGroup>              groupList = new JList<>();
    private JList<LJFriend>             friendsNotInList = new JList<>();
    private JList<LJFriend>             friendsInList = new JList<>();
    private JTabbedPane                 tabbedPane = new JTabbedPane();

    /* Menus */
    private JMenu friendsMenu;
    private JMenu groupsMenu;

    private final String[] friendsTableHeaderNames = {
        " ",
        L10N.FRIENDS_WINDOW_TABLE_USERNAME.s(),
        L10N.FRIENDS_WINDOW_TABLE_FULL_NAME.s(),
        "C",
        L10N.FRIENDS_WINDOW_TABLE_BIRTHDAY.s(),
    };

    private final String[] bannedTableHeaderNames = {
        L10N.FRIENDS_WINDOW_BANNED_TABLE_USER.s(),
        L10N.FRIENDS_WINDOW_BANNED_TABLE_COMMENT.s(),
    };

    public FriendsFrame(LJUserProfile profile, LJAccount account) {
        super(profile);
        createActions();
        createMenuBar();
        initComponents();
        createPopupMenu();

        m_account = account;

        getFriendList();
        stat = m_account.getCounters();

        changedFriends = new ArrayList<>();
        friendsIn = new ArrayList<>();
        friendsNotIn = new ArrayList<>();
        groupsCopy = new ArrayList<>();
        groupsChanged = false;
        groupsToDelete = new ArrayList<>();

        addAppEventListener(BanListUpdatedEvent.class, new AppEventListener() {
            @Override
            public void eventReceived(EventObject event) {
                new UpdateBanTableThread().execute();
            }
        });

        createFriendsTable();
        createGroupListModels();
        createBannedTableModel();

        updateFriendStatFields();
        reloadGroupsTab();
        setIconImage(Icon.FRIENDS_WINDOW_ICON.I().getImage());
        Options.getBounds(this);

        if (_friends.isEmpty() && profile.isOnline()) {
            if (JOptionPane.showConfirmDialog(this,
                    L10N.FRIENDS_WINDOW_INITIAL_LOAD_QUESTION.s(),
                    AboutDialog.PRODUCT_NAME,
                    JOptionPane.YES_NO_OPTION,
                    JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION) {
                update();
            }
        }
    }

    private void initComponents() {
        setTitle(L10N.FRIENDS_WINDOW_TITLE.s());

        tabbedPane.addChangeListener(new javax.swing.event.ChangeListener() {
            @Override
            public void stateChanged(javax.swing.event.ChangeEvent evt) {
                onTabbedPaneStateChanged();
            }
        });

        // Friends tab
        JPanel tab = new JPanel(new BorderLayout());

        friendsTable = new BaseTable();
        friendsTable.setShowGrid(false);
        friendsTable.setRowHeight(18);
        friendsTable.addMouseListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                friendsTableDoubleClick(evt);
            }
        });
        friendListScroller = new JScrollPane(friendsTable);
        friendListScroller.addComponentListener(new java.awt.event.ComponentAdapter() {
            @Override
            public void componentResized(java.awt.event.ComponentEvent evt) {
                TableUtil.adjustTableColumnSizes(friendsTable, 2, friendListScroller);
            }
        });
        friendListScroller.getViewport().setBackground(friendsTable.getBackground());

        tab.add(friendListScroller, BorderLayout.CENTER);
        tabbedPane.addTab(L10N.FRIENDS_WINDOW_FRIENDS_TAB.s(), tab);

        // Groups tab
        tab = new JPanel(new GridLayout());

        JPanel gp1 = new JPanel(new BorderLayout());
        gp1.setBorder(BorderFactory.createTitledBorder(L10N.FRIENDS_WINDOW_GROUPS_LABEL.s()));
        gp1.add(new JScrollPane(groupList), BorderLayout.CENTER);

        groupList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        groupList.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent evt) {
                onGroupListClicked();
            }
        });
        groupList.setDragEnabled(true);
        groupList.setDropMode(DropMode.ON_OR_INSERT);
        groupList.setTransferHandler(new GroupListTransferHandler(this));

        JPanel gp2 = new JPanel(new BorderLayout());
        gp2.setBorder(BorderFactory.createTitledBorder(L10N.FRIENDS_WINDOW_FRIENDS_NOT_IN_LABEL.s()));
        gp2.add(new JScrollPane(friendsNotInList), BorderLayout.CENTER);

        friendsNotInList.addMouseListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                if (evt.getClickCount() == 2) {
                    onAddToGroup();
                }
            }
        });
        friendsNotInList.setDragEnabled(true);
        friendsNotInList.setDropMode(DropMode.ON_OR_INSERT);
        friendsNotInList.setTransferHandler(new FriendsInNotInTransferHandler(this));

        JPanel gp3 = new JPanel(new BorderLayout());
        gp3.setBorder(BorderFactory.createTitledBorder(L10N.FRIENDS_WINDOW_FRIENDS_IN_LABEL.s()));
        gp3.add(new JScrollPane(friendsInList), BorderLayout.CENTER);

        friendsInList.addMouseListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                if (evt.getClickCount() == 2) {
                    onRemoveFromGroup();
                }
            }
        });
        friendsInList.setDragEnabled(true);
        friendsInList.setDropMode(DropMode.ON_OR_INSERT);
        friendsInList.setTransferHandler(new FriendsInNotInTransferHandler(this));

        SwingFactory.add(tab, gp1, gp2, gp3);

        groupList.setCellRenderer(new GroupListCellRenderer());
        friendsInList.setCellRenderer(new FriendListCellRenderer());
        friendsNotInList.setCellRenderer(new FriendListCellRenderer());
        tabbedPane.addTab(L10N.FRIENDS_WINDOW_GROUPS_TAB.s(), tab);

        // Banned users tab
        tab = new JPanel(new BorderLayout());

        banTable = new BaseTable();
        banTableScroller = new JScrollPane(banTable);

        banTableScroller.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent evt) {
                TableUtil.adjustTableColumnSizes(banTable, 1, banTableScroller);
            }
        });

        banTable.setRowHeight(18);
        banTable.setShowGrid(false);
        banTableScroller.getViewport().setBackground(banTable.getBackground());
        tab.add(banTableScroller, BorderLayout.CENTER);

        tabbedPane.addTab(L10N.FRIENDS_WINDOW_BANNED_TAB.s(), tab);


        // Status bar
        JPanel p1, p2, p3, p4, p5;
        statusBar.add(p1 = SwingFactory.createPanel(null, SwingFactory.LAYOUT.BOX_X,
                new JLabel(L10N.FRIENDS_WINDOW_FRIENDS_LABEL.s()),
                Box.createHorizontalStrut(3),
                friendCountEdit
            )
        );
        statusBar.add(p2 = SwingFactory.createPanel(null, SwingFactory.LAYOUT.BOX_X,
                Box.createHorizontalStrut(5),
                new JLabel(L10N.FRIENDS_WINDOW_MUTUAL_LABEL.s()),
                Box.createHorizontalStrut(3),
                mutualCountEdit
            )
        );
        statusBar.add(p3 = SwingFactory.createPanel(null, SwingFactory.LAYOUT.BOX_X,
                new JLabel(L10N.FRIENDS_WINDOW_FRIENDSOF_LABEL.s()),
                Box.createHorizontalStrut(3),
                friendofCountEdit
            )
        );
        statusBar.add(p4 = SwingFactory.createPanel(null, SwingFactory.LAYOUT.BOX_X,
                Box.createHorizontalStrut(5),
                new JLabel(L10N.FRIENDS_WINDOW_COMMUNITIES_LABEL.s()),
                Box.createHorizontalStrut(3),
                communityCountEdit
            )
        );
        statusBar.add(p5 = SwingFactory.createPanel(null, SwingFactory.LAYOUT.BOX_X,
                new JLabel(L10N.FRIENDS_WINDOW_SYNDICATED_LABEL.s()),
                Box.createHorizontalStrut(3),
                synCountEdit
            )
        );

        p1.setOpaque(false);
        p2.setOpaque(false);
        p3.setOpaque(false);
        p4.setOpaque(false);
        p5.setOpaque(false);

        getContentPane().add(SwingFactory.createToolBarPanel(
            createProfileToolBar(false, true),
            Box.createHorizontalGlue(),
            getBusyLabel()
        ), BorderLayout.NORTH);
        getContentPane().add(statusBar, BorderLayout.SOUTH);
        getContentPane().add(tabbedPane, BorderLayout.CENTER);
    }

    private void getFriendList() {
        _friends.clear();
        for (LJFriend f : m_account.getFriends()) {
            _friends.add(f);
        }
    }

    private void createActions() {
        addFriendAction = new AbstractAction(L10N.FRIENDS_WINDOW_ADD_FRIEND_ACTION.s(), Icon.ADD_FRIEND.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onAddFriend();
            }
        };
//        _addFriendAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, BloggingKit.getKeyMask()));

        removeFriendAction = new AbstractAction(L10N.FRIENDS_WINDOW_REMOVE_FRIEND_ACTION.s(), Icon.REMOVE_FRIEND.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onRemoveFriend();
            }
        };
        removeFriendAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, BloggingKit.getKeyMask()));

        banFriendAction = new AbstractAction(L10N.FRIENDS_WINDOW_BAN_FRIEND_ACTION.s(), Icon.BAN_FRIEND.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onBanFriend();
            }
        };

        friendJournalAction = new AbstractAction(L10N.FRIENDS_WINDOW_FRIEND_JOURNAL_ACTION.s(), Icon.EARTH.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                for (LJFriend f : getSelectedFriends()) {
                    openFriendLink(f, "");
                }
            }
        };

        friendInfoAction = new AbstractAction(L10N.FRIENDS_WINDOW_FRIEND_INFO_ACTION.s(), Icon.EARTH.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                String userInfoURL = m_account.getProperties().getUserInfoURL();
                for (LJFriend f : getSelectedFriends()) {
                    OSUtil.runBrowser(userInfoURL  + f.getUserName());
                }
            }
        };

        friendCalendarAction = new AbstractAction(L10N.FRIENDS_WINDOW_FRIEND_CALENDAR_ACTION.s(), Icon.EARTH.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                for (LJFriend f : getSelectedFriends()) {
                    openFriendLink(f, "calendar");
                }
            }
        };

        friendFriendsAction = new AbstractAction(L10N.FRIENDS_WINDOW_FRIEND_FRIENDS_ACTION.s(), Icon.EARTH.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                for (LJFriend f : getSelectedFriends()) {
                    openFriendLink(f, "friends");
                }
            }
        };

        addGroupAction = new AbstractAction(L10N.FRIENDS_WINDOW_ADD_GROUP_ACTION.s()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onAddGroup();
            }
        };
//        addGroupAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_A, BloggingKit.getKeyMask()));

        removeGroupAction = new AbstractAction(L10N.FRIENDS_WINDOW_REMOVE_GROUP_ACTION.s()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onRemoveGroup();
            }
        };
        removeGroupAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, BloggingKit.getKeyMask()));

        groupUpAction = new AbstractAction(L10N.FRIENDS_WINDOW_GROUP_UP_ACTION.s()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onGroupUp();
            }
        };
        groupUpAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_UP, BloggingKit.getKeyMask()));

        groupDownAction = new AbstractAction(L10N.FRIENDS_WINDOW_GROUP_DOWN_ACTION.s()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onGroupDown();
            }
        };
        groupDownAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, BloggingKit.getKeyMask()));

        groupTogglePublicAction = new AbstractAction("Toggle Public") {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onGroupTogglePublic();
            }
        };
        groupTogglePublicAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_P, BloggingKit.getKeyMask()));

        saveGroupsAction = new AbstractAction(L10N.FRIENDS_WINDOW_GROUP_SAVE_ACTION.s()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onSaveGroups();
            }
        };
        saveGroupsAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, BloggingKit.getKeyMask()));

        banAction = new AbstractAction(L10N.FRIENDS_WINDOW_BAN_FRIEND_ACTION.s(), Icon.EMPTY.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onBan();
            }
        };

        unbanAction = new AbstractAction(L10N.FRIENDS_WINDOW_UNBAN_ACTION.s(), Icon.EMPTY.i()) {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                onUnban();
            }
        };

        getUpdateAction().setEnabled(getProfile().isOnline());
        addFriendAction.setEnabled(getProfile().isOnline());
        removeFriendAction.setEnabled(getProfile().isOnline());
        banFriendAction.setEnabled(getProfile().isOnline());
        banAction.setEnabled(getProfile().isOnline());
        unbanAction.setEnabled(getProfile().isOnline());
    }

    private void createMenuBar() {
        createMenuBar(
            createFileMenu(),
            friendsMenu = createMenu(L10N.FRIENDS_WINDOW_TITLE.s(), null,
                addFriendAction,
                removeFriendAction,
                null,
                banFriendAction,
                null,
                friendJournalAction,
                friendInfoAction,
                friendCalendarAction,
                friendFriendsAction
            ),
            groupsMenu = createMenu(L10N.FRIENDS_WINDOW_GROUPS_MENU_TITLE.s(), null,
                addGroupAction,
                removeGroupAction,
                null,
                groupUpAction,
                groupDownAction,
                groupTogglePublicAction,
                null,
                saveGroupsAction
            ),
            createProfileMenu(),
            createToolsMenu(),
            createWindowMenu(),
            createHelpMenu()
        );

        groupsMenu.setVisible(false);
    }

    private void createPopupMenu() {
        friendListScroller.setComponentPopupMenu(createMenu(null, null,
            getUpdateAction(),
            null,
            addFriendAction,
            removeFriendAction,
            null,
            banFriendAction,
            null,
            friendJournalAction,
            friendInfoAction,
            friendCalendarAction,
            friendFriendsAction).getPopupMenu());

        banTableScroller.setComponentPopupMenu(createMenu(null, null,
            getUpdateAction(),
            null,
            banAction,
            unbanAction).getPopupMenu());
    }

    private void reloadGroupsTab() {
        groupsToDelete.clear();

        for (LJFriend f : changedFriends) {
            f.cancelChanges();
        }

        changedFriends.clear();

        /* Fill local copy of groups */
        groupBitMap = 0;
        groupsCopy.clear();
        for (int i = 0; i < m_account.getGroupsCount(); i++) {
            LJGroup g = m_account.getGroup(i);
            groupsCopy.add(g.clone());
            groupBitMap |= (1 << g.getNumber());
        }

        groupsChanged = false;
        groupListModel.fireListDataChanged();
        onGroupListClicked();
        updateGroupButtonsState();
    }

    /** Helper function returns currently selected friend if any */
    private LJFriend getSelectedFriend() {
        int index = this.friendsTable.getSelectedRow();
        if (index != -1) {
            index = friendsTable.convertRowIndexToModel(index);
            return _friends.get(index);
        } else {
            return null;
        }
    }

    private ImmutableList<LJFriend> getSelectedFriends() {
        final int[] indices = friendsTable.getSelectedRows();
        final ArrayList<LJFriend> selection = new ArrayList<>(indices.length);

        for (int ind : indices) {
            selection.add(_friends.get(friendsTable.convertRowIndexToModel(ind)));
        }
        return ImmutableList.copyOf(selection);
    }

    /** Helper function returns currently selected group if any */
    private LJGroup getSelectedGroup() {
        int index = this.groupList.getSelectedIndex();
        return ((index >= 0) && (index < this.groupsCopy.size()))? this.groupsCopy.get(index) : null;
    }

    private void createFriendsTable() {
        friendsListTableModel = new AbstractTableModel() {
            @Override
            public int getColumnCount() {
                return friendsTableHeaderNames.length;
            }

            @Override
            public int getRowCount() {
                return _friends.size();
            }

            @Override
            public Object getValueAt(int row, int col) {
                LJFriend friend = _friends.get(row);
                switch (col) {
                    case 0:
                        return LJUserProfile.getFriendStatusIcon(friend);
                    case 1:
                        if (friend.getType() == LJJournalType.IDENTITY) {
                            return friend.getIdentityDisplay();
                        } else {
                            return friend.getUserName();
                        }
                    case 2:
                        StringBuilder b = new StringBuilder(friend.getFullName());
                        if (!friend.getIdentityValue().isEmpty()) {
                            b.append(" [").append(friend.getIdentityValue()).append("]");
                        }
                        return b.toString();
                    case 3:
                        switch (friend.getType()) {
                            case COMMUNITY:
                                return Icon.COMMUNITY_ICON.i();
                            case SYNDICATED:
                                return Icon.SYNDICATED.i();
                            case NEWS:
                                return Icon.NEWS_INFO.i();
                            case IDENTITY:
                                switch (friend.getIdentityType()) {
                                    case OPENID:
                                        return Icon.OPENID.i();
                                    case FACEBOOK:
                                        return Icon.FACEBOOK.i();
                                    case TWITTER:
                                        return Icon.TWITTER.i();
                                    default:
                                        return null;
                                }
                            default:
                                return null;
                        }
                    case 4:
                        return friend.getBirthday();
                    default:
                        return null;
                }
            }

            @Override
            public String getColumnName(int column) {
                return friendsTableHeaderNames[column];
            }

            @Override
            public Class<?> getColumnClass(int col) {
                return (col == 0 || col == 3)? ImageIcon.class : String.class;
            }
        };

        friendsTable.setModel(friendsListTableModel);

        friendsTable.setRowSorter(new TableRowSorter<TableModel>(friendsListTableModel));
    }

    private void updateGroupButtonsState() {
        this.saveGroupsAction.setEnabled(this.groupsChanged);
        this.addGroupAction.setEnabled(this.groupsCopy.size() < 30);

        String tabTitle = L10N.FRIENDS_WINDOW_GROUPS_TAB.s();
        if (groupsChanged) {
            tabTitle += " *";
        }
        tabbedPane.setTitleAt(1, tabTitle);

////        int index = this.groupList.getSelectedIndex();
////        this.groupRemoveButton.setEnabled(index != -1);
    }

    private void createGroupListModels() {
        groupListModel = new FriendListModel<>(groupsCopy);
        friendsNotInListModel = new FriendListModel<>(friendsNotIn);
        friendsInListModel = new FriendListModel<>(friendsIn);

        groupList.setModel(groupListModel);
        friendsNotInList.setModel(friendsNotInListModel);
        friendsInList.setModel(friendsInListModel);
    }

    private void createBannedTableModel() {
        bannedTableModel = new DefaultTableModel() {
            @Override
            public int getColumnCount() {
                return bannedTableHeaderNames.length;
            }


            @Override
            public int getRowCount() {
                return getProfile().getBannedUsers().size();
            }

            @Override
            public boolean isCellEditable(int row, int col) {
                return (col == 1);
            }

            @Override
            public void setValueAt(Object aValue, int row, int columnIndex) {
                if (columnIndex == 1) {
                    LJBannedUser user = getProfile().getBannedUsers().get(row);
                    user.setComment((String)aValue);
                    onBannedTableUpdated();
                }
            }

            @Override
            public Object getValueAt(int row, int col) {
                LJBannedUser user = getProfile().getBannedUsers().get(row);
                return (col == 0)? user.getUserName() : user.getComment();
            }

            @Override
            public String getColumnName(int column) {
                return bannedTableHeaderNames[column];
            }

            @Override
            public Class<?> getColumnClass(int column) {
                return String.class;
            }
        };

        banTable.setModel(bannedTableModel);
        banTable.setCellEditor(new DefaultCellEditor(new JTextField()));
        TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(bannedTableModel);
        sorter.setSortable(1, false);
        banTable.setRowSorter(sorter);
    }


    private void onFriendListUpdated() {
        getFriendList();
//        sortFriends(m_sortColumn);

        friendsListTableModel.fireTableDataChanged();
        TableUtil.adjustTableColumnSizes(friendsTable, 2, friendListScroller);
        updateFriendStatFields();
        reloadGroupsTab();
        getProfile().storeFriends(m_account);
    }

    private void onBannedTableUpdated() {
        bannedTableModel.fireTableDataChanged();
        TableUtil.adjustTableColumnSizes(banTable, 1, banTableScroller);
        try {
            getProfile().storeBanned();
        } catch (Exception ex) {
            MainClass.handleException(this, ex);
        }
    }

    private void openFriendLink(LJFriend friend, String suffix) {
        StringBuilder b = new StringBuilder(m_account.getProperties().getUsersURL());
        b.append(friend.getUserName());
        b.append("/");
        b.append(suffix);
        OSUtil.runBrowser(b.toString());
    }

    private boolean canCloseGroupTab() {
        if (this.groupsChanged) {
            int res = JOptionPane.showConfirmDialog(this, "Save changes?");
            switch (res) {
                case JOptionPane.YES_OPTION:
                    onSaveGroups();
                    return true;

                case JOptionPane.NO_OPTION:
                    reloadGroupsTab();
                    return true;

                case JOptionPane.CANCEL_OPTION:
                    return false;
            }
        }
        return true;
    }

    private void onAddToGroup(ArrayList<LJFriend> ffs) {
        LJGroup g = getSelectedGroup();

        if (g != null) {
            for (LJFriend f : ffs) {
                if (!f.getEdit()) {
                    this.changedFriends.add(f);
                }

                f.edit();
                f.addToGroup(g);
            }

            this.friendsNotInList.setSelectedIndices(new int[0]);
            onGroupListClicked();
            this.groupsChanged = true;
            updateGroupButtonsState();
        }
    }

    private void onAddToGroup() {
        ArrayList<LJFriend> ffs = new ArrayList<>();
        for (LJFriend f : friendsNotInList.getSelectedValuesList()) {
            ffs.add(f);
        }
        onAddToGroup(ffs);
    }

    private void onRemoveFromGroup(ArrayList<LJFriend> ffs) {
        LJGroup g = getSelectedGroup();
        if (g != null) {
            for (LJFriend f : ffs) {
                if (!f.getEdit()) {
                    changedFriends.add(f);
                }

                f.edit();
                f.removeFromGroup(g);
            }

            friendsInList.setSelectedIndices(new int[0]);
            onGroupListClicked();
            groupsChanged = true;
            updateGroupButtonsState();
        }
    }

    private void onRemoveFromGroup() {
        ArrayList<LJFriend> ffs = new ArrayList<>();
        for (LJFriend f : friendsInList.getSelectedValuesList()) {
            ffs.add(f);
        }
        onRemoveFromGroup(ffs);
    }

    private void onGroupUp() {
        int index = groupList.getSelectedIndex();
        if (index > 0) {
            LJGroup g = groupsCopy.set(index - 1, groupsCopy.get(index));
            groupsCopy.set(index, g);
            groupsChanged = true;
            updateGroupButtonsState();
            groupListModel.fireListDataChanged();
            groupList.setSelectedIndex(index - 1);
        }
    }

    private void onGroupDown() {
        int index = groupList.getSelectedIndex();
        if (index < groupsCopy.size() - 1) {
            LJGroup g = groupsCopy.set(index + 1, groupsCopy.get(index));
            groupsCopy.set(index, g);
            groupsChanged = true;
            updateGroupButtonsState();
            groupListModel.fireListDataChanged();
            groupList.setSelectedIndex(index + 1);
        }
    }

    private void onSaveGroups() {
        try {
            m_account.saveGroups(this.groupsCopy, this.groupsToDelete, this.changedFriends);

            for (LJGroup g : this.groupsCopy) {
                g.setTemporary(false);
            }

            m_account.setGroups(this.groupsCopy);
            for (LJFriend f : this.changedFriends) {
                f.applyChanges();
            }

            this.groupsChanged = false;
            this.changedFriends.clear();
            updateGroupButtonsState();
            getProfile().storeFriends(m_account);
        } catch (IOException | LJException e) {
            for (LJFriend f : this.changedFriends) {
                f.cancelChanges();
            }
            handleException(e);
        }
    }

    private void onAddGroup() {
        if (this.groupsCopy.size() < 30) {
            String gName = JOptionPane.showInputDialog(this, "Enter Group Name:", AboutDialog.PRODUCT_NAME, JOptionPane.QUESTION_MESSAGE);
            if ((gName != null) && (gName.length() != 0)) {
                LJGroup g = new LJGroup(gName);
                g.setTemporary(true);

                // Find unused number
                int mask = 2;
                for (int num = 1; num <= 30; num++) {
                    if ((mask & this.groupBitMap) == 0) {
                        this.groupBitMap |= mask;
                        g.setNumber(num);
                        break;
                    }
                    mask = mask << 1;
                }

                this.groupsCopy.add(g);
                this.groupsChanged = true;
                groupListModel.fireListDataChanged();
                updateGroupButtonsState();
            }
        } else {
            JOptionPane.showMessageDialog(this, "No more groups can be created", AboutDialog.PRODUCT_NAME, JOptionPane.OK_OPTION);
        }
    }

    private void onRemoveGroup() {
        final LJGroup group = getSelectedGroup();

        if ((group != null) && MainClass.showConfirmDialog(this)) {
            int mask = 1 << group.getNumber();
            /* release number */
            this.groupBitMap ^= mask;

            for (LJFriend f : m_account.getFriends()) {
                if (!f.getEdit()) {
                    changedFriends.add(f);
                    f.edit();
                }
                f.removeFromGroup(group);
            }

            /* Schedule the group for deletion if it was not just created */
            if (!group.isTemporary()) {
                this.groupsToDelete.add(group);
            }
            this.groupsCopy.remove(group);
            this.groupsChanged = true;
            groupListModel.fireListDataChanged();
            updateGroupButtonsState();
            onGroupListClicked();
        }
    }

    private void onBanFriend() {
        new FriendOperationThread() {
            @Override
            protected Object doInBackground() throws Exception {
                m_account.banFriends(getSelectedFriends());
                return null;
            }
        }.execute();
    }

    private void onBan() {
        final String uName = JOptionPane.showInputDialog(this, "Enter user name", "User Name", JOptionPane.PLAIN_MESSAGE);
        if (uName != null) {
            // Find in the friend list first
            final LJFriend f = m_account.getFriend(uName);
            if (f != null) {
                new BanFriendThread(f).execute();
            } else {
                new BanNonFriendThread(uName).execute();
            }
        }

    }

    private void onUnban() {
        int index = banTable.getSelectedRow();
        if (index != -1) {
            LJBannedUser user = getProfile().getBannedUser(index);
            if (user != null) {
                new UnbanNonFriendThread(user.getUserName()).execute();
            }
        }
    }

    private void onRemoveFriend() {
        final ImmutableList<LJFriend> selFriends = getSelectedFriends();
        if ((selFriends.size() != 0) && MainClass.showConfirmDialog(this)) {
            new FriendOperationThread() {
                @Override
                protected Object doInBackground() throws Exception {
                    for (LJFriend f : selFriends) {
                        m_account.deleteFriend(f);
                    }
                    return null;
                }
            }.execute();
        }
    }

    public void onAddFriend() {
        LJFriend sf = getSelectedFriend();
        if ((sf != null) && (!sf.isFriendOf())) {
            sf = null;
        }

        final FriendDialog dial = new FriendDialog(this, sf, m_account, true);
        if (dial.showDialog()) {
            new FriendOperationThread() {
                @Override
                protected Object doInBackground() throws Exception {
                    m_account.addFriend(dial.getUserName(), dial.getFg(), dial.getBg(), dial.getGroupMask());
                    return null;
                }
            }.execute();
        }
    }

    private void updateFriendStatFields() {
        friendCountEdit.setText(Integer.toString(stat.getFriendCount()));
        mutualCountEdit.setText(Integer.toString(stat.getMutualCount()));
        friendofCountEdit.setText(Integer.toString(stat.getFriendOfCount() - stat.getCommunityCount()));
        communityCountEdit.setText(Integer.toString(stat.getCommunityCount()));
        synCountEdit.setText(Integer.toString(stat.getSyndicatedCount()));
        statusBar.validate();
    }

    @Override
    public void update() {
        if (this.selectedTab == 0) {
            new UpdateFriendsThread().execute();
        } else {
            new UpdateBanTableThread().execute();
        }
    }

    public void friendListUpdated() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                onFriendListUpdated();
            }
        });
    }

    private void friendsTableDoubleClick(MouseEvent evt) {
        if (evt.getClickCount() == 2) {
            final LJFriend f = getSelectedFriend();
            if ((f != null) && (!f.isFriendOf())) {
                final FriendDialog dial = new FriendDialog(this, f, m_account, false);
                if (dial.showDialog()) {
                    new FriendOperationThread() {
                        @Override
                        protected Object doInBackground() throws Exception {
                            m_account.editFriend(f, dial.getFg(), dial.getBg(), dial.getGroupMask());
                            return null;
                        }
                    }.execute();
                }
            }
        }
    }

    private void onGroupListClicked() {
        friendsIn.clear();
        friendsNotIn.clear();

        LJGroup group = getSelectedGroup();
        if (group != null) {
            for (LJFriend f: _friends) {
                if (!f.isFriendOf()) {
                    if (f.isInGroup(group)) {
                        friendsIn.add(f);
                    } else {
                        friendsNotIn.add(f);
                    }
                }
            }
        }

        Collections.sort(friendsIn, new LJFriendAlphabeticalComparator());
        Collections.sort(friendsNotIn, new LJFriendAlphabeticalComparator());

        friendsNotInListModel.fireListDataChanged();
        friendsInListModel.fireListDataChanged();
    }

    private void onGroupTogglePublic() {
        LJGroup g = getSelectedGroup();
        if (g != null) {
            g.setPublic(!g.isPublic());
            this.groupsChanged = true;
            updateGroupButtonsState();
            groupListModel.fireListDataChanged();
        }
    }

    private void onTabbedPaneStateChanged() {
        int selIndex = tabbedPane.getSelectedIndex();

        if ((selectedTab == 1) && (selIndex != 1)) {
            if (!canCloseGroupTab()) {
                tabbedPane.setSelectedIndex(this.selectedTab);
            }
        }

        selectedTab = tabbedPane.getSelectedIndex();
        getUpdateAction().setEnabled(this.selectedTab != 1 && getProfile().isOnline());

        friendsMenu.setVisible(selectedTab == 0);
        groupsMenu.setVisible(selectedTab == 1);
    }

    void swapGroups(int i1, int i2) {
        LJGroup g = this.groupsCopy.set(i2, this.groupsCopy.get(i1));
        this.groupsCopy.set(i1, g);
        this.groupsChanged = true;
        updateGroupButtonsState();
        this.groupListModel.fireListDataChanged();
        this.groupList.setSelectedIndex(i2);
    }

    void moveFriendsFromToGroup(ArrayList<LJFriend> ffs, JList list) {
        if (list == friendsNotInList) {
            onRemoveFromGroup(ffs);
        } else {
            onAddToGroup(ffs);
        }
    }
}
